/*	--- 使用Session & Cookie --- 

在【Web应用程序】中，我们经常要【跟踪用户身份】。

Q: 当【一个用户】登录成功后，如果【用户】继续访问【其他页面】，【Web程序】如何才能【识别】出【该用户身份】？

因为【HTTP协议】是一个【无状态协议】，即【Web应用程序】无法区分【收到的2个HTTP请求】是否是【同一个浏览器】发出的。

为了【跟踪用户状态】，【服务器】可以向【浏览器】分配【一个唯一ID】，并以【Cookie的形式】发送到【浏览器】，【浏览器】在【后续访问时】总是附带【此Cookie】，

这样，【服务器】就可以识别【用户身份】。


-----------------------------------------


#	Session

我们把这种基于【唯一ID】识别【用户身份】的【机制】称为【Session】。

【每个用户】第一次【访问服务器】后，会【自动获得】一个【Session ID】。

如果【用户】在【一段时间】内没有【访问服务器】，那么【Session】会【自动失效】

【下次】即使带着【上次分配的Session ID访问】，【服务器】也认为这是一个【新用户】，会分配【新的Session ID】。


【JavaEE的Servlet机制】内建了【对Session的支持】。

我们以【登录】为例，当【一个用户】登录成功后，我们就可以把【这个用户的名字】放入【一个HttpSession对象】，以便【后续访问其他页面】的时候，能直接从"HttpSession"取出【用户名】： */
@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {

	// 模拟一个database
	private Map<String, String> users = Map.of("bob","bob123", "alic","alice123", "Tom","Tomcat");

	// GET请求,显示的登录页
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		resp.setContentType("text/html");

		PrintWriter pw = resp.getWriter();
		pw.write("<h1>Sign In</h1>");
		pw.write("<form action=\"/signin\" method=\"post\">");
		pw.write("<p>Username: <input name = \"username\"></p>");
		pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>");
		pw.write("<p><button type=\"submit\">Sign In</button> <a href=\"/\">Cancel</a></p>");
		pw.write("</form>");

		pw.flush();
	}

	// 【POST请求】时,处理【用户登录】
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {

		String name = req.getParameter("username");
		String password = req.getParameter("password");

		String expectedPassword = users.get(name.toLowerCase());
			// 获取【数据库/缓存的Map<String, String>】的密码

		if (expectedPassword != null && expectedPassword.equals(password)) {
			// 登录校验成功
			req.getSession().setAttribute("user", name); 
				// HttpServlet.getSession().setAttribute("user", name);
			resp.sendRedirect("/");
				// 重定向
		} else {
			resp.sendError(HttpServletResponse.SC_FORBIDDEN);
		}
	}
}

/*
上述【SignInServlet】在【判断用户登录成功】后，立刻将【用户名】放入【当前HttpSession】中： */
HttpSession session = req.getSession();
session.setAttribute("user", name);

/*
在【IndexServlet】中，可以从【HttpSession】取出【用户名】： */
@WebServlet(urlPatterns = "/")
public class IndexServlet extends HttpServlet {
	protected void doGet(HttpServetRequest req, HttpServletResponse resp) throws ServletException,IOException {

		// 从【HttpSession】获取【当前用户名】
		String user = (String)req.getSession().getAttribute("user");

		resp.setContentType("text/html");
		resp.setCharacterEncoding("UTF-8");
		resp.setHeader("X-Powered-By", "JavaEE Servlet");

		PrintWriter pw = resp.getWriter();
		pw.write("<h1>Welcome, " + (user != null ? user : "Guest") + "</h1>");

		if (user == null) {
			// 未登录, 显示【登录链接】
			pw.write("<p><a href=\"/signin\">Sign In</a></p>");
		} else {
			// 已登录, 显示【登出连接】
			pw.write("<p><a href = \"/signout\">Sign Out</a></p>");
		}

		pw.flush();
	}
}


/*
如果【用户】已登录，可以通过【访问/signout】登出。

【登出逻辑】就是从【HttpSession中】移除【用户相关信息】： */
@WebServlet(urlPatterns = "/signout")
public class SignOutServlet extends HttpServlet {
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {

		// 从【HttpSession】移除【用户名】
		req.getSession().removeAttribute("user");
		resp.senRedirect("/");
	}
}


/*
对于Web应用程序来说，我们总是通过【HttpSession这个高级接口】访问【当前Session】。

如果要深入理解【Session原理】，可以认为【Web服务器】在【内存】中, 自动维护了【一个ID 到 HttpSession的映射表】，我们可以用下图表示：

           ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐

           │      ┌───────────────┐                │
             ┌───>│ IndexServlet  │<──────────┐
           │ │    └───────────────┘           ▼    │
┌───────┐    │    ┌───────────────┐      ┌────────┐
│Browser│──┼─┼───>│ SignInServlet │<────>│Sessions││
└───────┘    │    └───────────────┘      └────────┘
           │ │    ┌───────────────┐           ▲    │
             └───>│SignOutServlet │<──────────┘
           │      └───────────────┘                │

           └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘


而【服务器】识别【Session】的【关键】就是: 依靠一个【名为JSESSIONID的Cookie】。

在【Servlet】中【第一次调用req.getSession()】时，【Servlet容器】自动创建【一个Session ID】，然后通过【一个名为JSESSIONID的Cookie】发送给【浏览器】

这里要注意的几点是：

	· 【JSESSIONID】是由【Servlet容器】自动创建的，目的是【维护一个浏览器会话】，它和我们的【登录逻辑】没有关系; 

	· 【登录和登出的业务逻辑】是我们自己根据【HttpSession】是否存在【一个"user"的Key】判断的; 登出后，【Session ID】并不会改变; 

	· 即使没有【登录功能】，仍然可以使用【HttpSession】追踪【用户】。例如，放入一些【用户配置信息】等。

------------------------------

除了使用【Cookie机制】可以【实现Session】外，还可以通过【隐藏表单、URL末尾附加ID】来追踪【Session】。

这些【机制】很少使用，【最常用的Session机制】仍然是【Cookie】。

使用【Session】时，由于【服务器】把【所有用户的Session】都存储在【内存】中，如果遇到【内存不足】的情况，就需要把【部分不活动的Session】序列化到【磁盘】上，这会大大【降低服务器的运行效率】

因此，【放入Session的对象】要小，通常我们放入【一个简单的User对象】就足够了： */
public class User {
	public long id; // URI
	public String email;
	public String name;
}


/*
在使用【多台服务器】构成【集群】时，【使用Session】会遇到【一些额外的问题】。

通常，【多台服务器集群】使用【反向代理】作为【网站入口】：

                                     ┌────────────┐
                                ┌───>│Web Server 1│
                                │    └────────────┘
┌───────┐     ┌─────────────┐   │    ┌────────────┐
│Browser│────>│Reverse Proxy│───┼───>│Web Server 2│
└───────┘     └─────────────┘   │    └────────────┘
                		        │    ┌────────────┐
                                └───>│Web Server 3│
                                     └────────────┘

如果【多台Web Server】采用【无状态集群】，那么【反向代理】总是以【轮询方式】将【请求】依次转发给【每台Web Server】。

这会造成【一个用户】在【Web Server 1】存储的【Session信息】，在【Web Server 2和3】上并【不存在】，

即从【Web Server 1】登录后，如果【后续请求】被【转发】到【Web Server 2或3】，那么【用户】看到的仍然是【未登录状态】。

要解决这个问题，
【方案一】: 是在【所有Web Server】之间进行【Session复制】.
但这样会【严重消耗网络带宽】，并且，【每个Web Server的内存】均存储【所有用户的Session】，【内存使用率】很低。

【方案二】: 是采用【粘滞会话（Sticky Session）机制】，即【反向代理】在【转发请求】的时候，总是根据【JSESSIONID的值】判断，【相同的JSESSIONID】总是转发到【固定的Web Server】，但这需要【反向代理的支持】。

无论采用【何种方案】，【使用Session机制】，会使得【Web Server的集群】很难扩展

因此，【Session】适用于【中小型Web应用程序】。

对于【大型Web应用程序】来说，通常需要【避免使用Session机制】。


=========================================================


#	Cookie

实际上，【Servlet】提供的【HttpSession】本质上就是通过一个【名为JSESSIONID的Cookie】来跟踪【用户会话】的。

除了【JSESSIONID这个名称】外，【其他名称的Cookie】我们可以【任意使用】。

如果我们想要【设置一个Cookie】，例如，【记录用户选择的语】言，可以【编写一个LanguageServlet】： */
@WebServlet(urlPatterns = "/pref")
public class LanguageServlet extends HttpServlet {

	private static final Set<String> LANGUAGES = Set.of("en", "zh");

	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {

		String lang = req.getParameter("lang");

		if (LANGUAGES.contains(lang)) {
			// 创建一个【新的cookie】
			Cookie cookie = new Cookie("lang", lang);

			// 该Cookie生效的路径范围
			cookie.setPath("/");

			// 该Cookie有效期
			cookie.setMaxAge(8640000); // 8640000s = 100 day

			// setSecure(true); 
				//https协议需要

			// 将该Cookie添加到响应
			resp.addCookie(cookie);
		}

		resp.sendRedirect("/");
	}
}


/*
创建一个【新Cookie】时，除了【指定名称】和【值】以外，通常需要设置setPath("/")，
【浏览器】根据【此前缀】决定【是否发送Cookie】。

如果【一个Cookie】调用了setPath("/user/")，那么【浏览器】只有在【请求】【以/user/开头的路时】才会【附加此Cookie】。

通过setMaxAge()设置【Cookie的有效期】，【单位=秒】，最后通过resp.addCookie(Cookie cookie)把Cookie添加到【响应】。

如果【访问的是https网页】，还需要【调用setSecure(true)】，否则【浏览器】不会发送【该Cookie】。

因此，务必注意：【浏览器】在请求【某个URL】时，是否【携带指定的Cookie】，取决于【Cookie】是否【满足以下所有要求】：

	· 【URL前缀】是【设置Cookie时的Path】; 
	· Cookie在【有效期内】; 
	· Cookie【设置了secure】时, 必须【以https访问】。


我们可以在【浏览器】看到【服务器】发送的Cookie
	Chrome -> F12 -> Network -> Headers -> pref?lang... -> Set-Cookie: lang=zh; Max-Age=8640000; Expires=Fir ....

-----------------------

如果我们要【读取Cookie】

例如，在IndexServlet中，读取【名为lang的Cookie】以获取【用户设置的语言】，可以写一个【Method】如下： */
private String parseLanguageFromCookie(HttpServletRequest req) {

	// 获取【请求附带的所有Cookie】
	Cookie[] cookies = req.getCookies();

	// 如果获取到Cookie
	if (cookies != null) {
		// 循环每个Cookie
		for (Cookie cookie : cookies) {
			// 如果【Cookie名称】为【lang】
			if (cookie.getName().equals("lang")) { // Cookie是一个Map
				// 返回Cookie的值
				return cookie.getValue();
			}
		}
	}
	// 返回默认值
	return "en";
}

/*
可见，【读取Cookie】主要依靠【遍历HttpServletRequest附带的所有Cookie】。


===========================================================

#	练习

使用Session和Cookie */





/*============================================================


#	----- 使用Session和Cookie の 小结 -----

1. 【Servlet容器】提供了【Session机制】以【跟踪用户】; 
	(Session机制,仅适用于 中小型WebAPP)

2. 【默认的Session机制】是以【Cookie形式】实现的，【Cookie名称】为【JSESSIONID】; 

3. 通过【读写Cookie】可以在【客户端】设置【用户偏好等】。


----------------------------

汇总:

Session的用法:*/
	
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
		
	//设置Session(页面登录成功,建立Session):
	req.getSession().setAttribute("user", name);
	resp.senRedirect("/");


	//移除Session(登录后,用户退出,移除Session):
	req.getSession().removeAttribute("user");
	resp.senRedirect("/");
}

	
/*
Cookie的用法:

1-1.获取Cookies: */
private String parseLanguageFromCookie(HttpServletRequest req) {

	// 获取【请求附带的所有Cookie】
	Cookie[] cookies = req.getCookies();

	// 如果获取到Cookie
	if (cookies != null) {
		// 循环每个Cookie
		for (Cookie cookie : cookies) {
			// 如果【Cookie名称】为【lang】
			if (cookie.getName().equals("lang")) { // Cookie是一个Map
				// 返回Cookie的值
				return cookie.getValue();
			}
		}
	}
	// 返回默认值
	return "en";
}










